package com.awesome.network;

import com.awesome.network.annotation.Api;
import com.awesome.network.annotation.Binary;
import com.awesome.network.annotation.ConnectTimeout;
import com.awesome.network.annotation.Download;
import com.awesome.network.annotation.FileTarget;
import com.awesome.network.annotation.FixedParam;
import com.awesome.network.annotation.FixedParams;
import com.awesome.network.annotation.Get;
import com.awesome.network.annotation.Header;
import com.awesome.network.annotation.Json;
import com.awesome.network.annotation.LogLevel;
import com.awesome.network.annotation.Multipart;
import com.awesome.network.annotation.Param;
import com.awesome.network.annotation.Post;
import com.awesome.network.annotation.Range;
import com.awesome.network.annotation.ReadTimeout;
import com.awesome.network.annotation.Url;
import com.awesome.network.annotation.WriteTimeout;
import com.awesome.network.call.AbsCall;
import com.awesome.network.call.CallImpl;
import com.awesome.network.call.DownloadCallImpl;
import com.awesome.network.callback.OnProgressListener;
import com.awesome.network.interceptor.DownloadInterceptor;
import com.awesome.network.interceptor.LogInterceptor;
import com.awesome.network.util.RequestFactory;

import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;

/**
 * Author: JFangZ
 * Email:janezhang1901@163.com
 * Date: 2021/4/28 9:39
 * Description:
 */
public enum OkHttpHelper {
    SINGLETON;
    private OkHttpClient okHttpClient = new OkHttpClient.Builder().
            build();

    private final ArrayList<AbsCall<?>> callList = new ArrayList<AbsCall<?>>();

    private String getUrl(Method method, Object[] args, Class<?> clazz) {
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        // Parse params,find full url param.
        for (int i = 0; i < parameterAnnotations.length; i++) {
            for (Annotation annotation : parameterAnnotations[i]) {
                if (!(annotation instanceof Url)) {
                    continue;
                }
                return (String) args[i];
            }
        }
        String url = "";
        Api api = clazz.getAnnotation(Api.class);
        if (api != null) {
            // Append base.
            url += api.value();
        }
        Get get = method.getAnnotation(Get.class);
        Post post = method.getAnnotation(Post.class);
        // Append path.
        if (get != null) {
            url += get.value();
        } else if (post != null) {
            url += post.value();
        }
        return url;
    }

    private Map<String, String> getHeaderMap(Method method, Object[] args) {
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        Map<String, String> headerMap = new HashMap<>();
        // Parse headers.
        for (int i = 0; i < parameterAnnotations.length; i++) {
            for (Annotation annotation : parameterAnnotations[i]) {
                if (!(annotation instanceof Header)) {
                    continue;
                }
                String name = ((Header) annotation).value();
                headerMap.put(name, args[i].toString());
            }
        }
        return headerMap;
    }


    private Map<String, Object> getParamMap(Method method, Object[] args) {
        Map<String, Object> paramMap = new HashMap<>();
        // Parse fixed param.
        FixedParam fixedParam = method.getAnnotation(FixedParam.class);
        if (fixedParam != null) {
            paramMap.put(fixedParam.paramName(), fixedParam.paramValue());
        }
        FixedParams fixedParams = method.getAnnotation(FixedParams.class);
        for (int i = 0; fixedParams != null && i < fixedParams.value().length; i++) {
            paramMap.put(fixedParams.value()[i].paramName(), fixedParams.value()[i].paramValue());
        }
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        // Parse param.
        for (int i = 0; i < parameterAnnotations.length; i++) {
            for (Annotation annotation : parameterAnnotations[i]) {
                if (!(annotation instanceof Param)) {
                    continue;
                }
                String name = ((Param) annotation).value();
                paramMap.put(name, args[i]);
            }
        }
        return paramMap;
    }

    private OkHttpClient.Builder getOkHttpClientBuilder(Method method) {
        OkHttpClient.Builder builder = null;
        LogLevel logLevel = method.getAnnotation(LogLevel.class);
        // The log level is set,clone builder.
        if (logLevel != null) {
            builder = okHttpClient.newBuilder();
            int logInterceptorIndex = -1;
            LogInterceptor.Logger logger = null;
            Iterator<Interceptor> iterator = builder.interceptors().iterator();
            int index = 0;
            while (iterator.hasNext()) {
                Interceptor next = iterator.next();
                if (next instanceof LogInterceptor) {
                    // Get original logger.
                    logger = ((LogInterceptor) next).getLogger();
                    // Record interceptor original position.
                    logInterceptorIndex = index;
                    iterator.remove();
                }
                index++;
            }
            if (logInterceptorIndex != -1 && logger != null) {
                // Add interceptor with new level into original position.
                builder.interceptors().add(logInterceptorIndex, new LogInterceptor(logLevel.value(), logger));
            }
        }
        ConnectTimeout connectTimeout = method.getAnnotation(ConnectTimeout.class);
        // The connect timeout is set,clone builder.
        if (connectTimeout != null) {
            if (builder == null) {
                builder = okHttpClient.newBuilder();
            }
            builder.connectTimeout(connectTimeout.timeout(), connectTimeout.timeUnit());
        }
        ReadTimeout readTimeout = method.getAnnotation(ReadTimeout.class);
        // The read timeout is set,clone builder.
        if (readTimeout != null) {
            if (builder == null) {
                builder = okHttpClient.newBuilder();
            }
            builder.readTimeout(readTimeout.timeout(), readTimeout.timeUnit());
        }
        WriteTimeout writeTimeout = method.getAnnotation(WriteTimeout.class);
        // The write timeout is set,clone builder.
        if (writeTimeout != null) {
            if (builder == null) {
                builder = okHttpClient.newBuilder();
            }
            builder.writeTimeout(writeTimeout.timeout(), writeTimeout.timeUnit());
        }
        return builder;
    }

    private File getFileTarget(Method method, Object[] args) throws Exception {
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        // Parse file target.
        for (int i = 0; i < parameterAnnotations.length; i++) {
            for (Annotation annotation : parameterAnnotations[i]) {
                if (!(annotation instanceof FileTarget)) {
                    continue;
                }
                return (File) args[i];
            }
        }
        throw new Exception("Download request must have file target param.");
    }

    private long getRange(Method method, Object[] args) {
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        for (int i = 0; i < parameterAnnotations.length; i++) {
            for (Annotation annotation : parameterAnnotations[i]) {
                if (!(annotation instanceof Range)) {
                    continue;
                }
                return (long) args[i];
            }
        }
        return 0;
    }

    private OnProgressListener getOnProgressListener(Object[] args) {
        for (Object arg : args) {
            if (arg instanceof OnProgressListener) {
                return (OnProgressListener) arg;
            }
        }
        return null;
    }


    private AbsCall<?> getGetCall(Method method, Object[] args, Class clazz) throws Exception {
        // Get return type.
        Type genericReturnType = method.getGenericReturnType();
        if (!(genericReturnType instanceof ParameterizedType)) {
            throw new IllegalStateException("get return model type failure");
        }
        if (((ParameterizedType) genericReturnType).getActualTypeArguments().length == 0) {
            throw new IllegalStateException("get return model type failure");
        }
        // Get url.
        String url = getUrl(method, args, clazz);
        // Get headers.
        Map<String, String> headerMap = getHeaderMap(method, args);
        // Get params.
        Map<String, Object> paramMap = getParamMap(method, args);
        OkHttpClient.Builder builder = getOkHttpClientBuilder(method);
        if (method.getAnnotation(Download.class) == null) {
            // Plain get request.
            if (builder == null) {
                return new CallImpl<>(okHttpClient.newCall(RequestFactory.newGetRequest(url, headerMap, paramMap)), ((ParameterizedType) genericReturnType).getActualTypeArguments()[0]);
            } else {
                return new CallImpl<>(builder.build().newCall(RequestFactory.newGetRequest(url, headerMap, paramMap)), ((ParameterizedType) genericReturnType).getActualTypeArguments()[0]);
            }
        } else {
            // Download request.
            File fileTarget = getFileTarget(method, args);
            // Breakpoint to continue.
            long range = getRange(method, args);
            headerMap.put("RANGE", "bytes=" + range + "-");
            // Get download listener.
            OnProgressListener onProgressListener = getOnProgressListener(args);
            if (onProgressListener == null) {
                if (builder == null) {
                    return new DownloadCallImpl(okHttpClient.newCall(RequestFactory.newGetDownloadRequest(url, headerMap, paramMap)), fileTarget);
                } else {
                    return new DownloadCallImpl(builder.build().newCall(RequestFactory.newGetDownloadRequest(url, headerMap, paramMap)), fileTarget);
                }
            } else {
                // Add download progress interceptor.
                if (builder == null) {
                    builder = okHttpClient.newBuilder();
                }
                builder.interceptors().add(0, new DownloadInterceptor(range, onProgressListener));
                return new DownloadCallImpl(builder.build().newCall(RequestFactory.newGetDownloadRequest(url, headerMap, paramMap)), fileTarget);
            }
        }
    }


    private AbsCall<?> getPostCall(Method method, Object[] args, Class<?> clazz) {
        {
            // Get return type.
            Type genericReturnType = method.getGenericReturnType();
            if (!(genericReturnType instanceof ParameterizedType)) {
                throw new IllegalStateException("get return model type failure");
            }
            if (((ParameterizedType) genericReturnType).getActualTypeArguments().length == 0) {
                throw new IllegalStateException("get return model type failure");
            }
            // Get url.
            String url = getUrl(method, args, clazz);
            // Get headers.
            Map<String, String> headerMap = getHeaderMap(method, args);
            // Get params.
            Map<String, Object> paramMap = getParamMap(method, args);
            OkHttpClient.Builder builder = getOkHttpClientBuilder(method);
            Request request;
            if (method.getAnnotation(Json.class) != null) {
                // Content-Type is application/json.
                request = RequestFactory.newPostJsonRequest(url, headerMap, paramMap);
            } else if (method.getAnnotation(Multipart.class) != null) {
                // Upload file.
                // Get upload listener.
                OnProgressListener onProgressListener = getOnProgressListener(args);
                if (method.getAnnotation(Binary.class) == null) {
                    // Upload file by form-data.
                    request = RequestFactory.newPostFileRequest(url, headerMap, paramMap, onProgressListener);
                } else {
                    // Content-Type is application/octet-stream.
                    request = RequestFactory.newPostFileRequestBinary(url, headerMap, paramMap, onProgressListener);
                }
            } else {
                // Plain post request with form-data.
                request = RequestFactory.newPostRequest(url, headerMap, paramMap);
            }
            if (builder == null) {
                return new CallImpl<>(okHttpClient.newCall(request), ((ParameterizedType) genericReturnType).getActualTypeArguments()[0]);
            } else {
                return new CallImpl<>(builder.build().newCall(request), ((ParameterizedType) genericReturnType).getActualTypeArguments()[0]);
            }
        }
    }

    public <T> T getCaller(final Class<T> clazz) {
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                AbsCall<?> absCall = null;
                if (method.getAnnotation(Get.class) != null) {
                    absCall = getGetCall(method, args, clazz);
                } else if (method.getAnnotation(Post.class) != null) {
                    absCall = getPostCall(method, args, clazz);
                }
                return absCall;
            }
        });
    }

    /**
     * Author: JFangZ
     * Email:janezhang1901@163.com
     * Date:2021/4/30 12:55
     * Description:Cancel all request.
     */
    public void cancelAll() {
        for (AbsCall<?> absCall : callList) {
            absCall.cancel();
        }
    }
    public void addCall(AbsCall<?> call) {
        callList.add(call);
    }

    public void removeCall(AbsCall<?> call) {
        callList.remove(call);
    }

    public void setOkHttpClient(OkHttpClient okHttpClient) {
        this.okHttpClient = okHttpClient;
    }
}